package top.codedance.iotp.tools.terminal.util;

import top.codedance.iotp.common.util.ExportCertUtil;
import top.codedance.iotp.tools.terminal.TerminalApplication;
import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.X509Certificate;
import java.time.Period;
import java.util.Arrays;
import java.util.regex.Pattern;

public class KeyStoreLoader {

    private static final Pattern IP_ADDR_PATTERN = Pattern.compile(
            "^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");

    private static final String CLIENT_ALIAS = "codedance-opc-ua";
    private static final char[] PASSWORD = "codedance2023".toCharArray();

    private X509Certificate[] clientCertificateChain;
    private X509Certificate clientCertificate;
    private KeyPair clientKeyPair;
    private Path serverKeyStore;
    private Path exportCertFile;
    private Path exportPrivateFile;
    private Path exportPublicFile;
    private Path exportHash;

    public void initFilePath(Path baseDir){
        serverKeyStore = baseDir.resolve("codedance-opc-ua-client.pfx");
        exportCertFile = baseDir.resolve("codedance-opc-ua-client.cer");
        exportPrivateFile = baseDir.resolve("codedance-opc-ua-client.privateKey.txt");
        exportPublicFile = baseDir.resolve("codedance-opc-ua-client.publicKey.txt");
        exportHash = baseDir.resolve("codedance-opc-ua-client.hash.txt");
    }

    public KeyStoreLoader load(Path baseDir, int strength, String algorithm, int validity, boolean sign, String hash, String applicationName, String applicationUri) throws Exception {
        KeyStore keyStore = KeyStore.getInstance(algorithm); //PKCS12 JKS JCEKS DKS

        initFilePath(baseDir);

        TerminalApplication.syslogInfo("Loading KeyStore at " + serverKeyStore);

        if (!Files.exists(serverKeyStore)) {
            keyStore.load(null, PASSWORD);

            KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(strength);

            SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
                    .setCommonName(applicationName)
                    .setValidityPeriod(Period.ofYears(validity))
                    .setOrganization("codedance")
                    .setOrganizationalUnit("technology")
                    .setLocalityName("BeiJing")
                    .setStateName("CA")
                    .setCountryCode("US")
                    .setApplicationUri(applicationUri)
                    .addDnsName("localhost")
                    .addIpAddress("127.0.0.1");

            // Get as many hostnames and IP addresses as we can listed in the certificate.
            for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
                if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
                    builder.addIpAddress(hostname);
                } else {
                    builder.addDnsName(hostname);
                }
            }

            X509Certificate certificate = builder.build();

            keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate});
            try (OutputStream out = Files.newOutputStream(serverKeyStore)) {
                keyStore.store(out, PASSWORD);
            }
        } else {
            try (InputStream in = Files.newInputStream(serverKeyStore)) {
                keyStore.load(in, PASSWORD);
            }
        }

        ExportCertUtil.exportCert(keyStore, CLIENT_ALIAS, exportCertFile.toString());
        if(!Files.exists(exportPrivateFile) || !Files.exists(exportPublicFile)) {
            KeyPair keyPair = ExportCertUtil.getKeyPair(keyStore, CLIENT_ALIAS, PASSWORD);
            if(!Files.exists(exportPrivateFile)) {
                ExportCertUtil.exportPrivateKey(keyPair.getPrivate(), exportPrivateFile.toString());
            }
            if(!Files.exists(exportPublicFile)) {
                ExportCertUtil.exportPublicKey(keyPair.getPublic(), exportPublicFile.toString());
            }
        }

        Key clientPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
        if (clientPrivateKey instanceof PrivateKey) {
            clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);

            clientCertificateChain = Arrays.stream(keyStore.getCertificateChain(CLIENT_ALIAS))
                    .map(X509Certificate.class::cast)
                    .toArray(X509Certificate[]::new);

            PublicKey serverPublicKey = clientCertificate.getPublicKey();
            clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) clientPrivateKey);

            if(sign) {
                MessageDigest md = MessageDigest.getInstance(hash);
                byte[] certBytes = clientCertificate.getEncoded();
                byte[] certHash = md.digest(certBytes);
                try (OutputStream out = Files.newOutputStream(exportHash)) {
                    out.write(certHash);
                    out.flush();
                }
            }
        }

        return this;
    }

    public X509Certificate getClientCertificate() {
        return clientCertificate;
    }

    public X509Certificate[] getClientCertificateChain() {
        return clientCertificateChain;
    }

    public KeyPair getClientKeyPair() {
        return clientKeyPair;
    }

    public Path getServerKeyStore() {
        return serverKeyStore;
    }

    public Path getExportCertFile() {
        return exportCertFile;
    }

    public Path getExportPrivateFile() {
        return exportPrivateFile;
    }

    public Path getExportPublicFile() {
        return exportPublicFile;
    }

    public Path getExportHash() {
        return exportHash;
    }
}
